﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using SecureConnection.Utils;

namespace SecureConnection.Optimization
{
    public class Cache
    {
        private SortedList<int, List<byte[]>> cache = new SortedList<int, List<byte[]>>();
        private SortedList<int, List<int>> usage = new SortedList<int, List<int>>();

        public uint CacheSize { get; private set; }
        public uint MaxCacheSize { get; set; }
        public uint MinCacheSize { get; set; }

        private ulong cachedUsages = 0;
        private ulong totalUsages = 0;

        public double CacheEfficiency { get { if (cachedUsages == 0 || totalUsages == 0) return 0; else return Math.Round((double)cachedUsages / (double)totalUsages * (double)100, 2); } }

        public Cache()
        {
            this.CacheSize = 0;
            this.MaxCacheSize = 1024 * 1024 * 5; //5 MB
            this.MinCacheSize = 16;
        }

        public byte[] DecacheBuffer(byte[] data)
        {
            lock (cache)
            {
                ByteArrayReader br = new ByteArrayReader(data);

                totalUsages++;
                switch ((CacheProcessId)data[data.Length -1])
                {
                    case CacheProcessId.NotCached:
                        return br.ReadBytes(br.Data.Length - br.Offset - 1);
                    case CacheProcessId.PutInCache:
                        byte[] buffer = br.ReadBytes(br.Data.Length - br.Offset - 1);
                        if (!cache.ContainsKey(buffer.Length))
                        {
                            cache.Add(buffer.Length, new List<byte[]>());
                            usage.Add(buffer.Length, new List<int>());
                        }

                        cache[buffer.Length].Add(buffer);
                        usage[buffer.Length].Add(1);

                        CacheSize += (uint)buffer.Length;
                        while (CacheSize > MaxCacheSize)
                        {
                            int minUsageLength = 0;
                            int minUsageIndex = 0;

                            int minUsage = int.MaxValue;

                            for (int i = 0; i < cache.Count; i++)
                                for (int j = 0; j < cache.Values[i].Count; j++)
                                {
                                    if (usage.Values[i][j] < minUsage)
                                    {
                                        minUsage = usage.Values[i][j];
                                        minUsageLength = cache.Keys[i];
                                        minUsageIndex = j;
                                    }
                                }

                            CacheSize -= (uint)minUsageLength;
                            cache[minUsageLength].RemoveAt(minUsageIndex);
                        }

                        return buffer;
                    case CacheProcessId.ReadFromCache:
                        cachedUsages++;
                        int length = br.ReadInt();
                        int index = br.ReadInt();

                        usage[length][index]++;
                        return cache[length][index];
                    default:
                        throw new Exception("Error");
                }
            }
        }

        public unsafe byte[] CacheBuffer(byte[] buffer)
        {
            lock (cache)
            {
                totalUsages++;
                if (buffer.Length > MaxCacheSize || buffer.Length < MinCacheSize)
                {
                    Array.Resize(ref buffer, buffer.Length + 1);
                    buffer[buffer.Length - 1] = (byte)CacheProcessId.NotCached;
                    return buffer;
                }

                if (cache.ContainsKey(buffer.Length))
                {
                    fixed (byte* ptr1 = buffer)
                    {
                        for (int i = 0; i < cache[buffer.Length].Count; i++)
                        {
                            fixed (byte* ptr2 = cache[buffer.Length][i])
                            {
                                if (memcmp(ptr1, ptr2, (uint)buffer.Length) == 0)
                                {
                                    usage[buffer.Length][i]++;

                                    using (ByteArrayWriter bw = new ByteArrayWriter())
                                    {
                                        cachedUsages++;
                                        bw.WriteInt(buffer.Length);
                                        bw.WriteInt(i);
                                        bw.WriteByte((byte)CacheProcessId.ReadFromCache);
                                        return bw.ToByteArray();
                                    }
                                }
                            }
                        }
                    }
                }
                else
                {
                    cache.Add(buffer.Length, new List<byte[]>());
                    usage.Add(buffer.Length, new List<int>());
                }

                cache[buffer.Length].Add(buffer);
                usage[buffer.Length].Add(1);
                CacheSize += (uint)buffer.Length;

                while (CacheSize > MaxCacheSize)
                {
                    int minUsageLength = 0;
                    int minUsageIndex = 0;

                    int minUsage = int.MaxValue;

                    for (int i = 0; i < cache.Count; i++)
                        for (int j = 0; j < cache.Values[i].Count; j++)
                        {
                            if (minUsage > usage.Values[i][j])
                            {
                                minUsage = usage.Values[i][j];
                                minUsageLength = cache.Keys[i];
                                minUsageIndex = j;
                            }
                        }

                    CacheSize -= (uint)minUsageLength;
                    cache[minUsageLength].RemoveAt(minUsageIndex);
                }

                Array.Resize(ref buffer, buffer.Length + 1);
                buffer[buffer.Length - 1] = (byte)CacheProcessId.PutInCache;
                return buffer;
            }
        }     

        [DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)]
        public static extern unsafe int memcmp(byte* ptr1, byte* ptr2, uint count);
    }

    internal enum CacheProcessId
    {
        PutInCache,
        ReadFromCache,
        NotCached
    }
}
